Skip to content

feat: extension file reload — watch for plugin changes and hot-reload runtime#6347

Merged
yiliang114 merged 14 commits into
QwenLM:mainfrom
ZijianZhang989:feat/extension-file-reload
Jul 8, 2026
Merged

feat: extension file reload — watch for plugin changes and hot-reload runtime#6347
yiliang114 merged 14 commits into
QwenLM:mainfrom
ZijianZhang989:feat/extension-file-reload

Conversation

@ZijianZhang989

@ZijianZhang989 ZijianZhang989 commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a file watcher on extension directories so that editing extension files on disk is automatically detected. Content changes to commands, skills, and agent files are applied inline without user action. Configuration and hook changes prompt the user once to run /reload-plugins.

The runtime refresh logic that was previously inline in ExtensionManager (restart MCP servers, reload skill/subagent caches, refresh hierarchical memory) is extracted into a single refreshExtensionRuntime function used by all enable/disable/install/uninstall paths as well as the new /reload-plugins command.

The slash command reports a summary of what was loaded: extension count, commands, skills, agents, hooks, MCP servers, and LSP servers, in the user's locale.

Hook configuration is now reloaded on extension changes. Agent-scoped hooks are preserved across the reload, and a failure during hook rebuild restores the previous state.

Extension manager mutations (enable, disable, install, uninstall, update, scope changes, favorites, MCP server toggles) are now wrapped with a lifecycle listener so the file watcher can suppress itself during programmatic changes and restart watching after the mutation settles.

Why it's needed

Today there is no way to detect extension file changes on disk. Every edit to a command, skill, hook, or extension config requires the user to manually run /reload-plugins. During extension development, where you iterate on files rapidly, the manual step adds significant friction.

Extension runtime refresh was split across two private methods on ExtensionManager (refreshMemory / refreshTools) with slightly different call chains depending on the caller, and hooks were never reloaded mid-session. This consolidates the refresh into one function used by every path.

Reviewer Test Plan

How to verify

  1. Install an extension that has commands, skills, or agents. Run the CLI normally.
  2. Edit a file in the extension's commands/, skills/, or agents/ directory and save.
  3. Wait ~250ms — the change should take effect without any manual action.
  4. Edit the extension's qwen-extension.json, a hook file, or a context file.
  5. A message should appear: "Extensions changed on disk. Run /reload-plugins to apply updates."
  6. Run /reload-plugins — a summary should appear showing counts like "1 extension · 2 commands · 3 skills · 4 agents · 5 hooks · 1 extension MCP server".
  7. Link an extension from a local path (/extensions link), edit a file in the linked directory, and confirm the watcher detects the change.
  8. Enable and disable an extension — the file watcher should suppress itself during the operation and restart watching afterward.
  9. Run with --bare — no watcher should be started.

Evidence (Before & After)

Before: Editing a skill file in an extension has no effect. The user must type /reload-plugins manually.

After: The skill is auto-refreshed within ~250ms of saving. Config and hook changes prompt the user once with a specific message.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

npm run dev on macOS. Unit tests and i18n strict-parity tests pass.

Risk & Scope

  • Main risk or tradeoff: The chokidar watcher consumes file descriptors. On macOS the default limit is 256 FSEvents handles; with many linked extensions this could approach the limit. The watcher logs errors on failure but has no user-visible notification for ENOSPC.
  • Not validated / out of scope: MCP server startup failures during /reload-plugins are not surfaced to the user (only logged). Hook disable state set via /hooks is lost across extension reloads. No backend daemon — the watcher stops with the process.
  • Breaking changes / migration notes: ExtensionManager.refreshMemory() was removed. It had no external callers — it was only called internally by refreshTools().

Linked Issues

Refs #3696

中文说明

这个 PR 做了什么

为扩展目录增加了文件监控,编辑磁盘上的扩展文件可被自动检测。对 commands、skills、agent 文件的内容变更会内联自动应用,无需用户操作。配置和钩子变更会一次性提示用户运行 /reload-plugins

之前分散在 ExtensionManager 中的运行时刷新逻辑(重启 MCP 服务器、重新加载 skill/subagent 缓存、刷新层次化记忆)被提取为一个 refreshExtensionRuntime 函数,所有 enable/disable/install/uninstall 路径以及新的 /reload-plugins 命令都使用它。

斜杠命令会以用户的语言报告加载内容汇总:扩展数量、命令、技能、代理、钩子、MCP 服务器和 LSP 服务器。

钩子配置现在会在扩展变更时重新加载。代理作用域的钩子会在重载过程中保留,钩子重建失败时会恢复到之前的状态。

扩展管理器变更操作(启用、禁用、安装、卸载、更新、作用域变更、收藏、MCP 服务器开关)现在通过生命周期监听器包裹,使文件监控器能够在程序化变更期间抑制自身,并在变更结束后重新开始监控。

为什么需要

当前无法检测磁盘上的扩展文件变更。每次编辑命令、技能、钩子或扩展配置文件,都需要用户手动运行 /reload-plugins。在扩展开发中频繁迭代文件时,手动操作增加了显著的摩擦。

扩展运行时刷新分散在 ExtensionManager 的两个私有方法中(refreshMemory / refreshTools),调用链因调用方不同而略有差异,且钩子从未在会话中间被重新加载。此次将其整合为一个所有路径都使用的函数。

Reviewer 测试计划

如何验证

  1. 安装一个包含 commands、skills 或 agents 的扩展,正常启动 CLI。
  2. 编辑扩展 commands/skills/agents/ 目录下的文件并保存。
  3. 等待约 250ms——变更应无需手动操作即生效。
  4. 编辑扩展的 qwen-extension.json、钩子文件或上下文文件。
  5. 应出现提示消息:"磁盘上的扩展已变更。运行 /reload-plugins 来应用更新。"
  6. 运行 /reload-plugins——应显示类似 "1 个扩展 · 2 个命令 · 3 个技能 · 4 个代理 · 5 个钩子 · 1 个扩展 MCP 服务器" 的汇总。
  7. 从本地路径 link 一个扩展,在链接目录中编辑文件,确认 watcher 能检测到变更。
  8. 启用和禁用一个扩展——文件 watcher 应在操作期间抑制自身,操作结束后重新开始监控。
  9. 使用 --bare 运行——不应启动 watcher。

Before / After

Before:编辑扩展中的技能文件无任何效果。用户必须手动输入 /reload-plugins

After:技能在保存后约 250ms 内自动刷新。配置和钩子变更会一次性提示用户,并给出具体消息。

测试平台

操作系统 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

npm run dev 在 macOS 上。单元测试和 i18n 严格校验测试均通过。

风险与范围

  • 主要风险或权衡:chokidar watcher 消耗文件描述符。macOS 默认限制为 256 个 FSEvents handle;有大量 linked extension 时可能接近此限制。watcher 在失败时会记录错误,但 ENOSPC 没有用户可见通知。
  • 未验证 / 超出范围:/reload-plugins 期间 MCP 服务器启动失败不会提示用户(仅记录日志)。通过 /hooks 设置的钩子禁用状态在扩展重载后会丢失。没有后台守护进程——watcher 随进程退出而停止。
  • 破坏性变更 / 迁移说明:ExtensionManager.refreshMemory() 已移除。该方法无外部调用方——仅由 refreshTools() 内部调用。

关联 Issue

Refs #3696

… runtime

- Extract refreshExtensionRuntime to centralize MCP, skills, subagents, hooks, and memory refresh
- Add ExtensionFileWatcher (chokidar) for auto-detecting extension file changes
- Add ExtensionRefreshState with per-session scoped instance and mutation suppression
- Replace monkey-patching with ExtensionManager native mutation listeners
- Add /reload-plugins slash command with i18n-aware summary across all 9 locales
- Add auto-refresh of extension content (commands/skills/agents) on file change
- Add HookRegistry.reloadConfiguredHooks() with correct error recovery
- Fix async mutation pairing via id-based Map instead of LIFO stack
- Fix bootstrap watcher close() UB with queueMicrotask deferral
- Fix concurrent refresh with runningRef/pendingRef guard
- Fix error propagation from refreshExtensionContentRuntime to UI
- Fix isIgnored cross-platform path splitting (path.sep → regex)
- Fix wrong ExtensionMutationEvent type via import from core
- Fix addItem on unmounted component with mountedRef guard
- Set followSymlinks: false on chokidar watchers

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR touches core infrastructure (packages/core/src/extension/*, packages/core/src/hooks/*) at scale — 781 changed lines in core paths (437 source + 344 tests), exceeding the 500-line threshold for non-maintainer PRs.

Core refactors of this scope must be maintainer-initiated. The changes introduce a mutation listener pattern across ~15 ExtensionManager methods, a new extension-runtime-refresh module, and hook registry reload capabilities — these are architectural decisions that need maintainer design review first.

@ZijianZhang989 — the direction looks interesting, but the scope of core changes requires a maintainer to sign off on the design. Could you open an issue to discuss the approach before proceeding? Specifically:

  1. The mutation listener pattern wrapping every ExtensionManager public method
  2. The refreshExtensionRuntime extraction and its contract with callers
  3. The reloadConfiguredHooks rollback semantics in HookRegistry

Once there's alignment on the design, a maintainer can champion the implementation.

中文说明

此 PR 涉及核心基础设施(packages/core/src/extension/*packages/core/src/hooks/*)的大规模变更——核心路径共 781 行变更(437 行源码 + 344 行测试),超出非维护者 PR 的 500 行阈值。

此规模的核心重构必须由维护者发起。变更包括在约 15 个 ExtensionManager 方法中引入变更监听器模式、新增 extension-runtime-refresh 模块、以及钩子注册表的重载能力——这些都是需要维护者先进行设计评审的架构决策。

@ZijianZhang989 —— 方向看起来很有意思,但核心变更的规模需要维护者先确认设计方案。能否先开一个 issue 讨论方案?具体包括:

  1. 包裹每个 ExtensionManager 公开方法的变更监听器模式
  2. refreshExtensionRuntime 提取及其与调用方的契约
  3. HookRegistryreloadConfiguredHooks 的回滚语义

设计方案达成一致后,维护者可以推进实现。

Qwen Code · qwen3.7-max

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.

@wenshao

wenshao commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

No new Suggestion-level findings this round — all prior suggestions have been addressed or superseded.

— qwen3.7-max via Qwen Code /review

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 3 Critical issues around silent failure modes. The watcher architecture and mutation-wrapping design look solid overall, but three error paths silently swallow failures that previously propagated or that the user needs to know about. See inline comments.

Comment thread packages/cli/src/ui/commands/reload-plugins-command.ts
Comment thread packages/core/src/extension/extension-runtime-refresh.ts Outdated
Comment thread packages/core/src/extension/extension-runtime-refresh.ts Outdated
Comment thread packages/cli/src/ui/hooks/slashCommandProcessor.ts
Comment thread packages/cli/src/config/extension-refresh-state.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/cli/src/config/extension-runtime-reload.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/core/src/extension/extensionManager.ts
Comment thread packages/core/src/hooks/hookRegistry.ts
Comment thread packages/core/src/extension/extension-runtime-refresh.ts Outdated
Comment thread packages/cli/src/config/extension-refresh-state.ts
Comment thread packages/cli/src/config/extension-runtime-reload.ts
Comment thread packages/core/src/hooks/hookRegistry.ts
Comment thread packages/cli/src/config/extension-refresh-state.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/core/src/extension/extension-runtime-refresh.ts
Comment thread packages/core/src/extension/extension-runtime-refresh.ts Outdated
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/cli/src/ui/commands/reload-plugins-command.ts
Comment thread packages/core/src/extension/extensionManager.ts Outdated
Comment thread packages/cli/src/config/extension-file-watcher.test.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/cli/src/ui/hooks/slashCommandProcessor.ts
Comment thread packages/core/src/extension/extensionManager.ts
Comment thread packages/cli/src/i18n/locales/en.js
Comment thread packages/cli/src/config/extension-runtime-reload.test.ts
Comment thread packages/cli/src/config/extension-runtime-reload.test.ts
Comment thread packages/cli/src/config/extension-file-watcher.test.ts
@wenshao

wenshao commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

✅ Hands-on verification (real binary) — merge reference

I built the real qwen bundle from this PR's head (60750480, base 47f62a46) and drove the feature end-to-end. Core was resolved from the PR's own source (an esbuild alias over the workspace symlink), and I confirmed the PR code is actually in the bundle (addMutationListener, Extensions changed on disk, reload-plugins all present in dist/chunks/). This complements the existing "no blockers" review — it answers does it actually behave as described?

Verdict: yes. All three user-facing paths work in the live TUI, the author's 341 tests pass, and the suite provably bears weight under mutation. No functional regression observed.

1) Real TUI — the actual qwen binary, isolated HOME, a demo extension, edits driven on disk

real qwen TUI scenarios

# Trigger (on disk, live session) Observed in the real TUI
A add commands/farewell.md /farewell becomes resolvable inline, with no /reload-plugins (silent auto content-refresh)
B edit qwen-extension.json (config) ● Extensions changed on disk. Run /reload-plugins to apply updates. (one-shot prompt)
C run /reload-plugins ● Reloaded extensions: 1 extension · 2 commands · 1 skill · 1 agent · 0 hooks · 0 extension MCP servers · 0 extension LSP servers — the file hot-added in step A is counted

2) Tests — author's suites + a real-fs E2E + adversarial mutation

test evidence

  • 341 / 341 of the PR's own tests pass (core 195 + cli 146, 11 files).
  • Real chokidar E2E (added for this review): the shipped suite mocks chokidar + node:fs and fires synthetic events, so I wrote a harness that drives the real watcher against a real temp dir with real edits. 9/9, deterministic across 4 runscommands|skills|agents → content, hooks|qwen-extension.json|QWEN.md → stale, linked-extension source → content, .DS_Store|.swp|README.md → ignored.
  • Mutation testing — 5/5 caught: dropping skills from AUTO_REFRESH_DIRS (RED in both unit and the real E2E), isSuppressed()false, summary separator, throw hookReloadError, and dropping agent-scoped-hook preservation each turn the right suite RED and revert cleanly to green.
  • --bare → no watcher is gated in source (gemini.tsx:621, isBareMode(argv.bare) ? undefined : new ExtensionFileWatcher(...)) and covered by the bare-mode test.

Notes (non-blocking, for the record)

  • The one error path that is surfaced rather than swallowed — hook-reload failure (refreshExtensionRuntime re-throws hookReloadError) — is real and tested (mutation RED when the throw is removed). The remaining silent paths (ENOSPC, MCP startup, /hooks disable state across reload) are the author's explicitly-acknowledged out-of-scope items.
  • On macOS, FSEvents occasionally emits an incidental sibling re-stat under rapid multi-file writes. It is an OS-level watcher trait (reproduced with bare chokidar, not specific to this PR) and is absorbed by awaitWriteFinish (200 ms) + the "prompt once" idempotency in ExtensionRefreshState. Worst case is one extra /reload-plugins prompt, never a wrong classification.
  • By design, SUPPRESS_AFTER_MS = 1000 means a user file edit made within ~1 s after a programmatic enable/disable settles is intentionally ignored (anti-self-trigger). Sensible tradeoff; just noting it.

Bottom line: behavior matches the description, error surfacing on the hook path works, and the tests hold. LGTM to merge from a verification standpoint. 🚢

🇨🇳 中文完整版

✅ 真实二进制动手验证 —— 合并参考

我用本 PR head(60750480,base 47f62a46)构建了真实的 qwen bundle,端到端跑通了该特性。core 从 PR 自身源码解析(用一条 esbuild alias 覆盖工作区符号链接),并确认 PR 代码确实进了 bundle(addMutationListenerExtensions changed on diskreload-plugins 均在 dist/chunks/ 中命中)。这是对已有"no blockers" review 的补充——回答它是否真的按描述工作

结论:是。 三条用户可见路径在真实 TUI 中全部工作,作者的 341 个测试全绿,且变异测试证明测试套件真正承重。未发现功能回归。

1)真实 TUI —— 真 qwen 二进制、隔离 HOME、demo 扩展、在磁盘上真实编辑

(见上方第一张图)

# 触发(磁盘上,会话进行中) 真实 TUI 中的观察
A 新增 commands/farewell.md /farewell 无需 /reload-plugins 即内联可用(静默自动内容刷新)
B 编辑 qwen-extension.json(配置) ● Extensions changed on disk. Run /reload-plugins to apply updates.(一次性提示)
C 运行 /reload-plugins ● Reloaded extensions: 1 extension · 2 commands · 1 skill · 1 agent · 0 hooks · 0 extension MCP servers · 0 extension LSP servers——步骤 A 中热加的文件被计入

2)测试 —— 作者套件 + 真实文件系统 E2E + 对抗式变异

(见上方第二张图)

  • PR 自带测试 341 / 341 全绿(core 195 + cli 146,共 11 个文件)。
  • 真实 chokidar E2E(为本次评审新增): 现有套件把 chokidar + node:fs 都 mock 了、fire 合成事件,所以我写了一个 harness,用 watcher 对临时目录做编辑。9/9,连续 4 次确定性通过——commands|skills|agents → contenthooks|qwen-extension.json|QWEN.md → stale、软链扩展源 → content.DS_Store|.swp|README.md → 忽略
  • 变异测试——5/5 全部被捕获:AUTO_REFRESH_DIRS 删掉 skills(单测真实 E2E 同时变红)、isSuppressed()false、汇总分隔符、throw hookReloadError、以及删掉 agent 作用域 hook 的保留——每一个都让对应套件变红,还原后干净转绿。
  • --bare → 不启动 watcher 在源码中有门控(gemini.tsx:621isBareMode(argv.bare) ? undefined : new ExtensionFileWatcher(...)),并有 bare-mode 测试覆盖。

备注(非阻塞,留档)

  • 唯一被上抛而非吞掉的错误路径——hook 重载失败(refreshExtensionRuntime 重新 throw hookReloadError)——是真实且被测试的(删掉 throw 后变异变红)。其余静默路径(ENOSPC、MCP 启动、/hooks 禁用状态跨重载丢失)是作者已明确声明的超范围项。
  • 在 macOS 上,FSEvents 在快速多文件写入时偶发对 sibling 文件的连带 re-stat。这是操作系统层面的 watcher 特性(用裸 chokidar 也能复现,非本 PR 特有),被 awaitWriteFinish(200ms)+ ExtensionRefreshState 的"只提示一次"幂等吸收。最坏情况只是多一次 /reload-plugins 提示,绝不会误分类。
  • 按设计,SUPPRESS_AFTER_MS = 1000 意味着在程序化 enable/disable 结束后约 1 秒内的用户文件编辑会被有意忽略(防自触发)。合理的权衡,仅作说明。

总结: 行为与描述一致,hook 路径的错误上抛有效,测试站得住。从验证角度 LGTM,可以合并。🚢

Verified locally on macOS with a from-source qwen bundle; screenshots are faithful renders of the real captured terminal output.

@wenshao

wenshao commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Third triage pass on this PR. Since the last run (July 6), there's been a /review cycle, a maintainer's end-to-end tmux verification on Linux, and a merge conflict resolution. The code has matured significantly. Running through the gates again with the current state.

Template looks good ✓ — all required sections filled in, bilingual description, risk disclosure, test-plan steps with OS matrix.

Problem is real: extension developers currently have to type /reload-plugins after every file edit on disk. That's a friction point during rapid iteration, and it's referenced in #3696. Not theoretical.

Direction is aligned — auto-detecting extension file changes and hot-reloading content while prompting for config/hooks is exactly how developers expect a plugin system to behave. No product-direction concerns.

Size: core paths (packages/core/src/extension/, packages/core/src/hooks/) carry 383 production lines across 4 files — below the 500-line escalation threshold. Full PR is ~1,647 production lines (36 files including ~270 lines of i18n translation data and ~800 lines of tests), which triggers the non-blocking large-PR advisory. The scope is cohesive though — splitting would leave an incomplete feature.

Approach: chokidar file watcher with a three-way classification (auto-refresh for commands/skills/agents, stale-prompt for config/hooks, ignore for everything else) plus a shared ExtensionRefreshState event bus between watcher and UI is the right architecture. The mutation lifecycle pattern (paired start/end IDs) for watcher suppression during programmatic changes is a clean abstraction. Consolidating refreshMemory() + refreshTools() into one refreshExtensionRuntime() is a necessary cleanup to support the new code paths. I don't see a materially simpler path.

Moving to code review. 🔍

中文说明

第三次 triage。自上次运行以来,经历了 /review 周期、维护者在 Linux 上的端到端 tmux 验证,以及合并冲突解决。代码已显著成熟。

模板完整 ✓。问题真实存在:扩展开发者目前每次编辑磁盘上的文件都需要手动输入 /reload-plugins,这在快速迭代时是摩擦点,关联 #3696。方向对齐——自动检测扩展文件变更并对内容热重载、对配置/钩子提示手动重载,正是开发者对插件系统的预期。

规模:核心路径 383 行生产代码(4 个文件),低于 500 行升级阈值。完整 PR 约 1,647 行生产代码(36 个文件,含约 270 行 i18n 翻译数据和约 800 行测试),触发非阻塞大 PR 建议。范围内聚——拆分会导致功能不完整。

方案:chokidar 文件监听器加三路分类(commands/skills/agents 自动刷新,config/hooks 提示重载,其他忽略)加上监听器与 UI 之间的共享 ExtensionRefreshState 事件总线是正确的架构。变更生命周期模式(配对 start/end ID)用于在程序化变更期间抑制监听器是简洁的抽象。将 refreshMemory() + refreshTools() 合并为一个 refreshExtensionRuntime() 是支持新代码路径的必要清理。没有看到更简路径。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: I'd use chokidar to watch the user extension directory plus linked extension source directories. Classify each file change as either "content" (commands, skills, agents — safe to hot-reload silently) or "config" (manifest, hooks, context files — needs full /reload-plugins because it can affect the slash command list, hook registry, and cache coherently). Add a /reload-plugins command for the full rebuild. Extract the scattered refresh logic from ExtensionManager into one shared function. Use a small state object between watcher and UI for debouncing and notification. Suppress the watcher during programmatic mutations (install, enable, disable) to avoid false positives.

The PR's implementation matches this closely and goes further in good ways:

  • The mutation lifecycle with paired IDs in a Map correctly handles nested mutations (install triggering enable internally) without relying on stack order. Better than a simple depth counter.
  • Atomic cache replacement in refreshCache() — builds the new map first, swaps it in only after success. Eliminates the partial-state window the old code had.
  • Generation guard on watcher events prevents stale async callbacks from operating on outdated state after restartWatching().
  • Bootstrap watcher for the case where the extensions directory doesn't exist yet — watches the parent and restarts when the directory appears.
  • HookRegistry.reloadConfiguredHooks() correctly snapshots agent-scoped entries, clears configured entries, reprocesses, restores enabled state, and rolls back on failure. This is the kind of careful implementation that prevents subtle bugs.
  • ExtensionRefreshState has thoughtful edge-case handling: reload-in-progress deferral, post-reload re-emission of pending events, failure-state preservation for retry, suppression windows with depth tracking.
  • The error contract in refreshExtensionRuntime() is explicit and tested: MCP failures propagate, LSP partial failures propagate, hooks surface after settle, skills/subagents/memory are best-effort.

The ExtensionRuntimeRefreshConfig API change (from getToolRegistry().restartMcpServers() to reinitializeMcpServers(getSettingsMcpServers())) aligns with the settings-change hot reload path. Tests cover the new contract.

One issue found: in updateExtension's finally block, fs.promises.rm(tempDir) can throw before endMutation() runs, leaving the watcher permanently suppressed for the session. The /review bot flagged this as a suggestion but it wasn't applied. Fix: wrap the rm in its own try/catch so endMutation() always fires:

} finally {
  if (tempDir) {
    try {
      await fs.promises.rm(tempDir, { recursive: true, force: true });
    } catch {
      // temp dir cleanup best-effort; endMutation must always run
    }
  }
  endMutation();
}

Unit Tests

357 tests across 11 suites, all passing. Coverage includes happy paths, failure modes (MCP fatal, LSP partial failure, hook rollback, cache failure preservation), lifecycle boundaries (mutation events around enable/disable/install/uninstall/update), state preservation (reload-in-progress deferral, post-reload re-emission), and content-refresh serialization with the 5-pass limit.

Test count grew from 341 → 357 across review iterations — the author added tests specifically for the edge cases reviewers flagged.

Real-Scenario Testing

Relying on maintainer verification (wenshao, July 8, on Linux with the real esbuild bundle in tmux). The maintainer drove all 9 test-plan steps with the actual CLI and reported:

A) /reload-plugins output:
   "Reloaded extensions: 1 extension · 1 command · 1 skill · 1 agent · 1 hook ·
    0 extension MCP servers · 0 extension LSP servers"
   — all 7 counts correct, singular/plural correct

B) Config edit (qwen-extension.json):
   → one-time prompt: "Extensions changed on disk. Run /reload-plugins to apply updates."
   — NOT silently applied ✅

C) Content edit (add commands/bye.toml while running):
   → /bye auto-appears in command popup within ~250ms, no prompt, no manual reload ✅

Mechanism proof + negative control:

  • Watcher debug log confirms correct classification: commands/*.tomlaction: 'auto' (silent hot-reload); qwen-extension.jsonaction: 'stale' (prompt).
  • qwen --bare produces 0 EXTENSION_FILE_WATCHER log lines while other debug categories are active — the watcher is simply never constructed. Isolates ExtensionFileWatcher as the sole source of reload behavior (test-plan step 9 ✅).
  • Revert-proof: reverting each core file to base (keeping the PR tests) fails exactly the new tests — hookRegistry.ts → 3 fail, extensionManager.ts → 7 fail, extension-runtime-refresh.ts → 8 fail. Tests meaningfully lock behavior.

No correctness issues found by the maintainer. Clean merge, load-bearing tests, behavior matches spec. 🐧 Linux status: ⚠️ → ✅.

I didn't duplicate this testing — the maintainer's verification is more authoritative than anything I could orchestrate in headless CI for this particular feature (it requires live filesystem mutations to extension directories with chokidar debounce timing).

中文说明

代码审查

独立方案: 使用 chokidar 监听用户扩展目录及链接扩展源目录。将每个文件变更分为"内容"类(commands、skills、agents——可安全静默热重载)或"配置"类(清单、钩子、上下文文件——需要完整的 /reload-plugins,因为它可能影响斜杠命令列表、钩子注册表和缓存一致性)。添加 /reload-plugins 命令用于完整重建。将分散的刷新逻辑提取到一个共享函数。使用小型状态对象在监听器和 UI 之间进行防抖和通知。在程序化变更(安装、启用、禁用)期间抑制监听器以避免误报。

PR 的实现与此高度一致,并在以下方面有好的改进:

  • 配对 ID 的变更生命周期正确处理嵌套变更(安装内部触发启用),不依赖栈顺序。
  • refreshCache() 中的原子缓存替换——先构建新 map,仅在成功后交换。消除了旧代码的部分状态窗口。
  • 代数守卫防止 restartWatching() 后过期异步回调操作过期状态。
  • 引导监听器覆盖扩展目录尚不存在的情况。
  • HookRegistry.reloadConfiguredHooks() 正确快照 agent 作用域条目、清空、重新处理、恢复启用状态、失败时回滚。
  • ExtensionRefreshState 具备重载进行中延迟、重载后重新发射待处理事件、失败状态保留、带深度跟踪的抑制窗口等周到的边缘情况处理。
  • refreshExtensionRuntime() 的错误契约明确且已测试。

发现一个问题: updateExtensionfinally 块中,fs.promises.rm(tempDir) 可能抛异常导致 endMutation() 不执行,使 watcher 在会话期间被永久抑制。/review 机器人已标记此问题但未被应用。修复方法:将 rm 包装在自己的 try/catch 中,确保 endMutation() 始终执行。

单元测试

11 个测试套件共 357 个测试全部通过。覆盖正常路径、失败模式(MCP 致命、LSP 部分失败、钩子回滚、缓存失败保留)、生命周期边界、状态保留和内容刷新序列化。

真实场景测试

依赖维护者验证(wenshao,7月8日,Linux,真实 esbuild 产物,tmux)。维护者驱动了所有 9 个测试计划步骤并报告:/reload-plugins 输出正确(7 项计数、单复数正确),配置编辑触发一次性提示而非静默生效,内容编辑约 250ms 内自动出现在命令弹窗中。qwen --bare 产生 0 条 watcher 日志(反向对照)。反证测试确认测试有效锁定行为。未发现正确性问题。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Stepping back to look at the whole picture.

This is a well-scoped feature that solves a real pain point. Extension developers iterating on skill files, commands, or agents shouldn't have to manually reload after every save — that's table stakes for a plugin system. The problem is documented in #3696 and the PR addresses it directly.

The implementation is careful where it needs to be. The three-way file classification (auto / stale / ignore) is the right split — content files are safe to hot-reload, but hooks and config changes need a coordinated full rebuild because they can affect command routing, security-sensitive hook execution, and cache coherency. The ExtensionRefreshState state machine handles the genuinely complex interactions between watcher events, debouncing, programmatic mutation suppression, reload-in-progress deferral, and failure preservation. The hook reload logic with agent-scoped preservation and rollback-on-failure is the kind of implementation that prevents the subtle bugs that plague hot-reload systems.

My independent proposal matched the PR's architecture closely. I don't see a simpler path that would solve this problem. The scope feels right — the refreshMemory / refreshTools consolidation isn't drive-by refactoring, it's necessary for the new code paths to have a single refresh entry point.

What gives me confidence: the maintainer's tmux verification is thorough — real TUI output, mechanism proof via debug logs, negative control with --bare, and revert-proof test locking. That's more authoritative than anything I could reproduce in headless CI for a feature that requires live filesystem mutations with chokidar debounce timing. The author has been responsive to feedback across 14 commits, progressively hardening the state machine, clarifying failure modes, and tightening mutation boundaries.

The one issue I flagged — fs.promises.rm in updateExtension's finally block potentially skipping endMutation() — is a real bug but low-severity. Temp dir cleanup failures are uncommon, the impact is watcher suppression until CLI restart, and the fix is trivial.

At 383 production lines in core paths and ~1,647 total production lines, this is a substantial contribution from a fork. But the code is cohesive, well-tested (357 tests), and the maintainer has already verified it end-to-end on Linux and recommends merge.

Approving. The fs.promises.rm fix can be addressed in a follow-up or before merge at the maintainer's discretion.

中文说明

退一步看全局。

这是一个范围合理的功能,解决了真实的痛点。迭代技能文件、命令或代理的扩展开发者不应每次保存后都手动重载——这是插件系统的基本要求。问题在 #3696 中有记录,PR 直接解决了它。

实现在需要仔细的地方很仔细。三路文件分类(auto / stale / ignore)是正确的分割——内容文件可安全热重载,但钩子和配置变更需要协调的完整重建,因为它们可能影响命令路由、安全敏感的钩子执行和缓存一致性。ExtensionRefreshState 状态机处理了监听器事件、防抖、程序化变更抑制、重载进行中延迟和失败保留之间真正复杂的交互。带 agent 作用域保留和失败回滚的钩子重载逻辑能防止热重载系统中常见的微妙 bug。

我的独立方案与 PR 架构高度一致。没有看到更简路径。范围合理——refreshMemory / refreshTools 的合并是必要的,为新代码路径提供单一刷新入口。

给予信心的因素:维护者的 tmux 验证全面——真实 TUI 输出、通过调试日志的机制证明、--bare 反向对照、反证测试锁定。作者通过 14 个提交积极响应反馈,逐步加固状态机、澄清失败模式并收紧变更边界。

标记的一个问题——updateExtension finally 块中的 fs.promises.rm 可能跳过 endMutation()——是真实但低严重度的 bug。临时目录清理失败不常见,影响是 watcher 在 CLI 重启前被抑制,修复简单。

核心路径 383 行、总计约 1,647 行生产代码是来自 fork 的实质性贡献。但代码内聚、测试充分(357 个测试),且维护者已在 Linux 上端到端验证并建议合并。

批准。fs.promises.rm 修复可在后续或合并前由维护者酌情处理。

Qwen Code · qwen3.7-max

@wenshao

wenshao commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

Comment thread packages/cli/src/config/extension-runtime-reload.ts
Comment thread packages/cli/src/config/extension-runtime-reload.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts
Comment thread packages/cli/src/config/extension-file-watcher.ts Outdated
Comment thread packages/core/src/extension/extension-runtime-refresh.ts Outdated
Comment thread packages/cli/src/ui/hooks/slashCommandProcessor.ts
Comment thread packages/cli/src/gemini.tsx Outdated
Comment thread packages/cli/src/config/extension-refresh-state.ts
Comment thread packages/cli/src/config/extension-runtime-reload.ts
Comment thread packages/core/src/extension/extensionManager.ts
Comment thread packages/cli/src/config/extension-refresh-state.ts
Comment thread packages/cli/src/ui/hooks/slashCommandProcessor.ts
@ZijianZhang989 ZijianZhang989 force-pushed the feat/extension-file-reload branch from 0bcd48a to d79c005 Compare July 7, 2026 06:29
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

Resolved import conflict in slashCommandProcessor.ts by combining
the PR's extension reload imports (AppEvent, t, refreshExtensionContentRuntime,
EXTENSION_RELOAD_FAILED_REASON, ExtensionRefreshState) with main's expanded
commands.js import (parseStackedSlashCommands, MAX_STACKED_SKILLS). Both
sets of imports are used in the file.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge Conflict Resolution Summary — PR #6347

Base branch

main

Merge performed

git merge origin/main into qwen-resolve/pr-6347

Conflicted file

packages/cli/src/ui/hooks/slashCommandProcessor.ts (1 conflict, lines 58–72)

Resolution

The conflict was in the import block at the top of the file. Both sides extended the same import { parseSlashCommand } from '../../utils/commands.js' line differently:

  • HEAD (PR branch) added extension-reload imports: AppEvent, t, refreshExtensionContentRuntime, EXTENSION_RELOAD_FAILED_REASON, ExtensionRefreshState — all used later in the file for the extension hot-reload feature.
  • origin/main expanded the commands.js import to also pull in parseStackedSlashCommands and MAX_STACKED_SKILLS — used later in the file for the stacked slash-skill invocations feature (feat(cli): support stacked slash-skill invocations #6361).

Resolution: Combined both sides. The parseSlashCommand import was kept in main's expanded multi-line form (adding parseStackedSlashCommands and MAX_STACKED_SKILLS), and all four PR-specific import statements (AppEvent, t, refreshExtensionContentRuntime, EXTENSION_RELOAD_FAILED_REASON/ExtensionRefreshState) were preserved immediately after.

Commit

6c38cae43merge: integrate origin/main into feat/extension-file-reload

Auto-merged files (no manual intervention needed)

  • packages/cli/src/i18n/locales/en.js, fr.js, ja.js, zh-TW.js, zh.js
  • packages/cli/src/ui/AppContainer.tsx
  • packages/cli/src/ui/commands/types.ts
  • packages/cli/src/ui/hooks/slashCommandProcessor.test.ts

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

wenshao
wenshao previously approved these changes Jul 7, 2026
@yiliang114 yiliang114 dismissed qwen-code-ci-bot’s stale review July 7, 2026 09:10

Dismissing: this review was submitted before the triage skill fix (#6369) was merged. Under the updated rules, this PR (feat:, 437 production lines excluding tests) does not trigger the Tier 1 hard block. The "500-line threshold" cited here no longer applies to feat-type PRs. See #6365 for context.

Comment thread packages/core/src/extension/extension-runtime-refresh.ts Outdated
Comment thread packages/core/src/extension/extension-runtime-refresh.ts Outdated
Comment thread packages/core/src/extension/extensionManager.ts
wenshao
wenshao previously approved these changes Jul 8, 2026

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Suggestions — commit ad2236476

File Issue Suggested fix
packages/core/src/extension/extensionManager.ts:1628 updateExtension finally block: fs.promises.rm(tempDir) can throw before endMutation(), leaving the watcher permanently suppressed Wrap fs.promises.rm in its own try/catch so endMutation() always runs

Needs Human Review (low confidence):

  • packages/cli/src/config/extension-refresh-state.ts + packages/cli/src/ui/commands/reload-plugins-command.ts + packages/cli/src/ui/AppContainer.tsx — Three sites create new ExtensionRefreshState() as a fallback. If any fires, the event bus disconnects silently. Consider adding a console.warn in each fallback.
  • packages/cli/src/config/extension-refresh-state.ts:89markExtensionsReloadFailed has no reloadInProgress precondition guard. All current callers are correct, but the API could be hardened.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

✅ Verification report — extension file-watch / hot-reload

I verified this PR end-to-end by merging it into current origin/main, running the full new unit surface with real vitest, and then driving the real esbuild bundle in a tmux TTY on Linux. All the behaviors in the Reviewer Test Plan reproduce. Recommend merge.

  • Merge base: origin/main @ 880b06ed4 + PR ad2236476 → merges cleanly (mergeable: MERGEABLE).
  • Env: Linux, Node 22, real dist/cli.js bundle (v0.19.7), isolated QWEN_HOME, a fixture extension demo-ext with commands/, skills/, agents/, and a manifest hook.

1) Real TUI behavior (A, B, C)

runtime

# Action Observed Verdict
A /reload-plugins Reloaded extensions: 1 extension · 1 command · 1 skill · 1 agent · 1 hook · 0 extension MCP servers · 0 extension LSP servers — all 7 counts, correct singular/plural (after adding a command it became 2 commands)
B edit qwen-extension.json (config/hook) one-time prompt Extensions changed on disk. Run /reload-plugins to apply updates.not silently applied
C add commands/bye.toml while running /bye auto-appears in the command popup ([demo-ext] say goodbye (auto-added while running)) within ~250ms, no prompt, no manual reload

2) Mechanism proof + negative control + unit suites

proof

  • Classification is correct at runtime (watcher debug log): editing commands/*.tomlaction: 'auto' (silent hot-reload); editing qwen-extension.jsonaction: 'stale' (prompt). This is exactly the content-vs-config split the design intends.
  • Negative control — qwen --bare: the same edits produce 0 EXTENSION_FILE_WATCHER log lines, while the debug file is otherwise full (50 lines across SKILL_MANAGER, FILE_COMMAND_LOADER, …). Logging is active — the watcher is simply never constructed under --bare. This isolates ExtensionFileWatcher as the sole cause of the reload behavior (PR test-plan step 9 ✅).
  • Unit suites (real vitest on the merge result): 326/326 pass across the 10 new/changed files.
  • Revert-proof (load-bearing): swapping each modified core file back to base (keeping the PR test) fails exactly the new tests —
    • hookRegistry.ts → 3 fail (reloadConfiguredHooks is not a function, incl. the error-recovery / agent-scope-preservation cases)
    • extensionManager.ts → 7 fail (addMutationListener is not a function + the atomic cache-swap test)
    • extension-runtime-refresh.ts → 8 fail (LSP/hook error surfacing + the getToolRegistry()reinitializeMcpServers() API change)

Notes for the maintainer

  • 🐧 Linux status can go from ⚠️ → ✅. The watcher, classification, auto-reload, prompt, and --bare gate all behave correctly on Linux via chokidar.
  • Breaking-change claim is accurate: ExtensionManager.refreshMemory() has no remaining callers anywhere in packages/ — removal is safe.
  • Intentional asymmetry worth documenting: once a config/hook change has set "refresh needed", subsequent content edits stop silently hot-reloading (markExtensionContentChanged bails while extensionRefreshNeeded is true) — they get folded into the next /reload-plugins. Correct, but non-obvious.
  • Latency nuance: end-to-end content refresh is the 250 ms debounce plus chokidar awaitWriteFinish (200 ms stability) plus the refresh, so it's closer to ~450–500 ms in practice than the "~250 ms" in the description — cosmetic only.
  • The PR's self-disclosed limitations (no user-visible ENOSPC notice, MCP start failures only logged during reload, /hooks disable-state lost across reload, watcher dies with the process) all hold and are reasonable for this scope.
  • Linked extensions (test-plan step 7): covered by 3 dedicated watcher unit tests (linked-source watch roots, linked-source change → refresh, inactive-link exclusion). I could not drive extensions link at runtime here because it blocks on a workspace-trust prompt with no non-interactive flag — but the linked path shares the same classification/refresh pipeline I verified for user-dir extensions.

No correctness issues found. Clean merge, load-bearing tests, behavior matches the spec. 👍

中文版(点击展开)

✅ 验证报告 —— 扩展文件监控 / 热重载

我通过将本 PR 合并进当前 origin/main、用真实 vitest 跑完全部新增单测、并在 Linuxtmux TTY 中驱动真实 esbuild 产物完成了端到端验证。Reviewer 测试计划中的所有行为都能复现。建议合并。

  • 合并基线: origin/main @ 880b06ed4 + PR ad2236476,干净合并(mergeable: MERGEABLE)。
  • 环境: Linux、Node 22、真实 dist/cli.js(v0.19.7)、隔离的 QWEN_HOME,以及一个包含 commands/skills/agents/ 和清单 hook 的 fixture 扩展 demo-ext

1)真实 TUI 行为(A / B / C)—— 见上方第一张图

# 操作 观察结果 结论
A /reload-plugins Reloaded extensions: 1 extension · 1 command · 1 skill · 1 agent · 1 hook · 0 extension MCP servers · 0 extension LSP servers——七项计数齐全,单复数正确(新增一个命令后变为 2 commands
B 编辑 qwen-extension.json(配置/hook) 一次性提示 Extensions changed on disk. Run /reload-plugins to apply updates.——不会静默生效
C 运行中新增 commands/bye.toml /bye 约 250ms 内自动出现在命令弹窗([demo-ext] say goodbye (auto-added while running)),无提示、无需手动重载

2)机制证明 + 反向对照 + 单测 —— 见上方第二张图

  • 运行时分类正确(watcher 调试日志):编辑 commands/*.tomlaction: 'auto'(静默热重载);编辑 qwen-extension.jsonaction: 'stale'(提示)。这正是设计所要的“内容 vs 配置”分流。
  • 反向对照 qwen --bare 相同的编辑产生 0EXTENSION_FILE_WATCHER 日志,而调试文件本身有 50 行其它日志(SKILL_MANAGERFILE_COMMAND_LOADER 等)。日志系统是开着的——只是 --bare 下 watcher 根本没被构造。这把 ExtensionFileWatcher 单独锁定为热重载行为的唯一来源(测试计划第 9 步 ✅)。
  • 单测(对合并结果跑真实 vitest):10 个新增/改动文件共 326/326 通过。
  • 反证(证明测试有效锁定行为): 将每个改动的 core 文件换回 base(保留 PR 的测试),恰好只让新增测试失败——
    • hookRegistry.ts → 3 失败(reloadConfiguredHooks is not a function,含错误回滚 / 保留 agent 作用域 hook 的用例)
    • extensionManager.ts → 7 失败(addMutationListener is not a function + 原子缓存交换用例)
    • extension-runtime-refresh.ts → 8 失败(LSP/hook 错误上抛 + getToolRegistry()reinitializeMcpServers() 的 API 变更)

给维护者的说明

  • 🐧 Linux 状态可从 ⚠️ 升级为 ✅。 watcher、分类、自动重载、提示、--bare 门控在 Linux 上(chokidar)均表现正确。
  • 破坏性变更声明属实: ExtensionManager.refreshMemory()packages/ 中已无任何调用方,移除安全。
  • 一处值得写进文档的有意行为: 一旦发生配置/hook变更并置位“需要刷新”,后续的内容编辑将不再静默热重载(markExtensionContentChangedextensionRefreshNeeded 为真时直接返回),而是并入下一次 /reload-plugins。逻辑正确,但不够直观。
  • 延迟细节: 内容刷新的端到端时延是 250ms 去抖 加上 chokidar awaitWriteFinish(200ms 稳定期)再加刷新耗时,实际接近 ~450–500ms,而非描述里的“~250ms”——仅为文案层面。
  • PR 自述的局限(ENOSPC 无用户可见提示、重载期间 MCP 启动失败仅记日志、/hooks 禁用状态在重载后丢失、watcher 随进程退出)均成立,且在本 PR 范围内合理。
  • Link 扩展(测试计划第 7 步): 由 3 个专门的 watcher 单测覆盖(link 源监控根、link 源变更 → 刷新、未激活 link 的排除)。此处未能在运行时驱动 extensions link,因为它会阻塞在工作区信任提示且无非交互开关——但 link 路径与我已验证的 user-dir 路径共用同一套分类/刷新管线。

未发现正确性问题。 干净合并、测试有效锁定、行为符合规范。👍

Verified on Linux by driving the real bundle (merge of origin/main + this PR) in tmux; unit runs are real vitest on the merge result.

@wenshao

wenshao commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

One minor fix recommended before merge: wrap fs.promises.rm(tempDir) in updateExtension's finally block with its own try/catch so endMutation() always runs (prevents permanent watcher suppression if temp dir cleanup fails). The maintainer can decide whether to address this now or in a follow-up.

@yiliang114

Copy link
Copy Markdown
Collaborator

macOS tmux verification addendum — hooks / MCP / LSP

I ran one extra hands-on pass for the runtime pieces that were not covered deeply in the earlier tmux report: extension hooks, extension MCP, and extension LSP.

  • PR head tested: ad2236476
  • Env: macOS, real qwen from this PR's source bundle, real tmux TTY, isolated HOME / QWEN_HOME
  • Startup: node scripts/start.js --sandbox=false --experimental-lsp

Results

  • /reload-plugins after extension runtime changes reported:
Reloaded extensions: 1 extension · 2 commands · 0 skills · 0 agents · 1 hook · 1 extension MCP server · 1 extension LSP server
  • MCP: /mcp showed the extension server as connected:
Extension MCPs
  tmux-empty-mcp · ✓ connected

The MCP server log confirmed a real restart and JSON-RPC handshake/list flow: initialize, notifications/initialized, prompts/list, resources/list, tools/list.

  • LSP: /lsp showed the extension LSP server as ready:
| Server | Command | Languages      | Status |
|--------|---------|----------------|--------|
| node   | node    | tmux-empty-lsp | READY  |

The LSP server log confirmed initialize, initialized, and workspace/didChangeWorkspaceFolders.

  • Hooks: after editing the extension hook config and running /reload-plugins, submitting a normal prompt executed the UserPromptSubmit command hook before the model request:
UserPromptSubmit: hook evidence prompt

The following model call failed with Connection error. (cause: fetch failed) because this smoke test used a dummy local model endpoint. That is expected and happens after the hook execution path above.

Verdict: this extra tmux pass covers the reload path for hooks, extension MCP reconnect, and extension LSP initialization. No new correctness issue found from these runtime checks.

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful follow-up here.

Approving from my side as well. I did an additional macOS tmux pass covering hooks, extension MCP, and extension LSP on the current head, and I don't see a correctness blocker for merge. The PR is also already mergeable, with the active required-looking checks green or skipped as expected for this run.

A few follow-ups are still worth tracking, but I don't think they need to block this PR:

  • updateExtension should eventually wrap temp-dir cleanup so endMutation() always runs even if fs.promises.rm(tempDir) throws.
  • The fallback new ExtensionRefreshState() paths could use a warning/assertion, since a fallback state would silently disconnect the event bus.
  • markExtensionsReloadFailed could be hardened with a reloadInProgress guard.
  • The CLI /reload-plugins path and daemon POST /workspace/extensions/refresh path now share the lower-level runtime refresh through refreshTools(), but their product semantics still differ. The daemon/ACP path currently swallows refreshTools() failures and returns { ok: true }, while the CLI path surfaces reload failures. Worth aligning in a follow-up.
  • The config/hook-stale behavior and actual debounce latency are worth documenting, since they are intentional but not obvious.

These can be handled as polish/follow-up issues. The current implementation and test coverage look good enough to merge.

@yiliang114

yiliang114 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Adding the visual evidence that should have been included with my approval.

These are screenshots rendered from the real tmux pane/log captures used for the macOS verification pass.

Reload / command discovery

reload summary and stale prompt

auto-added command popup

MCP / LSP

extension MCP connected

extension LSP ready

Hook execution / runtime logs

hook execution in tmux

runtime logs for hook, MCP, and LSP

@yiliang114 yiliang114 added this pull request to the merge queue Jul 8, 2026
Merged via the queue into QwenLM:main with commit 151d269 Jul 8, 2026
84 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants